{
  "openapi": "3.0.3",
  "info": {
    "title": "Conversor IAE/CNAE API",
    "description": "API REST para convertir y consultar códigos fiscales españoles IAE ↔ CNAE 2025, correspondencias CNAE 2009, régimen de módulos (estimación objetiva), obligaciones fiscales (modelos AEAT) y calendario de plazos.\n\nEN: REST API to convert and look up Spanish tax activity codes (IAE ↔ CNAE 2025), CNAE 2009 correspondences, módulos regime, fiscal obligations (AEAT modelos) and the fiscal deadline calendar.\n\n## Autenticación\nLos endpoints con clave requieren la cabecera `X-API-Key: cvr_…`. Obtén una clave gratuita (sin tarjeta) en `POST /signup` o en https://www.conversoriaecnae.es/precios.\n\n## Planes y cuotas\n- **Free**: 250 peticiones/día (sin tarjeta)\n- **Profesional**: 29€/mes — 2.000 peticiones/día\n- **Empresa**: 79€/mes — 10.000 peticiones/día\n- **Enterprise**: personalizado\n\nLímite de ráfaga según plan: 10 (Free/Desarrollador), 30 (Profesional) y 100 (Empresa/Enterprise) peticiones/minuto por clave. La cuota diaria se reinicia a las 00:00 (Europe/Madrid). Las respuestas 429 incluyen la cabecera `Retry-After` (segundos).\n\nLos endpoints `cnae-2009`, `bulk`, `kyb/enrich` y `onboarding/activity` requieren plan Profesional o superior (403 en plan Free). Los endpoints `modulos` y `obligaciones` están disponibles en el plan Free (requieren clave, pero cualquier plan sirve). Los endpoints `calendario`, `signup` y `subscribe` son públicos.\n\nNota: el plan gratuito (valor interno `free`, mostrado como \"Free\" en las respuestas de la API) se comercializa públicamente como \"Desarrollador\" en las páginas de precios.",
    "version": "1.0.0",
    "contact": {
      "name": "Conversor IAE CNAE",
      "email": "brian@conversoriaecnae.es",
      "url": "https://www.conversoriaecnae.es/precios"
    },
    "license": {
      "name": "CC-BY-4.0",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    },
    "termsOfService": "https://www.conversoriaecnae.es/terminos-condiciones"
  },
  "servers": [
    {
      "url": "https://www.conversoriaecnae.es/api/v1",
      "description": "Producción"
    }
  ],
  "tags": [
    { "name": "Códigos", "description": "Detalle de códigos IAE, CNAE 2025 y CNAE 2009, y lookup masivo." },
    { "name": "Búsqueda", "description": "Búsqueda híbrida (léxica + semántica) sobre los catálogos IAE y CNAE." },
    { "name": "Fiscal", "description": "Régimen de módulos, obligaciones (modelos AEAT) y calendario de plazos." },
    { "name": "Cuenta", "description": "Alta de clave, suscripción a planes de pago y consulta de uso." },
    { "name": "B2B", "description": "Endpoints de enriquecimiento para bancos (KYB) y SaaS de facturación / onboarding. Requieren plan Profesional o superior." }
  ],
  "security": [
    { "ApiKeyAuth": [] }
  ],
  "paths": {
    "/iae/{code}": {
      "get": {
        "operationId": "getIaeByCode",
        "summary": "Detalle de un código IAE",
        "description": "Devuelve el detalle de un código IAE (epígrafe). El código admite forma con o sin puntos (`501.3` o `5013`). Si un código numérico corresponde a varias filas (p. ej. aparece en secciones Empresarial y Profesional), responde 409 con `candidates[]`; usa `?slug=` para desambiguar. Los campos `associated_cnae` y `related_iae` requieren plan Profesional o superior.",
        "tags": ["Códigos"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/IaeCodePath" },
          { "$ref": "#/components/parameters/SlugQuery" }
        ],
        "responses": {
          "200": {
            "description": "Detalle del código IAE",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IaeDetail" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/AmbiguousCode" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/cnae/{code}": {
      "get": {
        "operationId": "getCnaeByCode",
        "summary": "Detalle de un código CNAE 2025",
        "description": "Devuelve el detalle de un código CNAE 2025 con sus epígrafes IAE asociados y equivalencias CNAE 2009. El código admite forma con o sin puntos (`68.20` o `6820`). Los campos `associated_iae`, `related_cnae` y `cnae_2009` requieren plan Profesional o superior.",
        "tags": ["Códigos"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/CnaeCodePath" }
        ],
        "responses": {
          "200": {
            "description": "Detalle del código CNAE 2025",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CnaeDetail" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/cnae-2009/{code}": {
      "get": {
        "operationId": "getCnae2009Correspondence",
        "summary": "Correspondencia CNAE 2009 → CNAE 2025",
        "description": "Devuelve las correspondencias de un código CNAE 2009 (obsoleto) hacia CNAE 2025. Requiere plan Profesional o superior.",
        "tags": ["Códigos"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/Cnae2009CodePath" }
        ],
        "responses": {
          "200": {
            "description": "Correspondencias CNAE 2009 → CNAE 2025",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Cnae2009Correspondence" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PaidRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/search": {
      "get": {
        "operationId": "searchCodes",
        "summary": "Buscar códigos IAE/CNAE",
        "description": "Búsqueda híbrida sobre los catálogos IAE y/o CNAE 2025. El parámetro `q` requiere al menos 2 caracteres.",
        "tags": ["Búsqueda"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Texto de búsqueda (mínimo 2 caracteres).",
            "schema": { "type": "string", "minLength": 2, "example": "albañilería" }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Catálogo a consultar.",
            "schema": { "type": "string", "enum": ["iae", "cnae", "both"], "default": "both" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Número máximo de resultados (1–50).",
            "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultados de búsqueda",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SearchResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/modulos/{code}": {
      "get": {
        "operationId": "getModulosByCode",
        "summary": "Régimen de módulos de un código IAE",
        "description": "Devuelve el régimen de módulos (estimación objetiva) para un código IAE: módulos IRPF/IVA, umbrales e inteligencia fiscal. Disponible en el plan Free (requiere una clave de API, cualquier plan sirve). Usa `?slug=` para desambiguar códigos que mapean a varias filas (409).",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/IaeCodePath" },
          { "$ref": "#/components/parameters/SlugQuery" }
        ],
        "responses": {
          "200": {
            "description": "Régimen de módulos del código IAE",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ModulosDetail" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/AmbiguousCode" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/tarifa-at-ep/{code}": {
      "get": {
        "operationId": "getTarifaAtEpByCode",
        "summary": "Tarifa AT/EP (cotización por cuenta ajena) de un código CNAE",
        "description": "Devuelve el tipo de cotización AT/EP (Accidentes de Trabajo / Enfermedades Profesionales, D.A. 61ª TRLGSS, Cuadro I) aplicable a un código CNAE 2025. Esta tarifa es la que paga la EMPRESA por sus TRABAJADORES POR CUENTA AJENA en la actividad — no es la cuota RETA del autónomo (que aplica el tipo fijo del 1,30%, ver `nota_reta`). La resolución usa un fallback de prefijo (código exacto → 3 dígitos → 2 dígitos), sin ambigüedad de sección/slug. Requiere plan Profesional o superior.",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/CnaeCodePath" }
        ],
        "responses": {
          "200": {
            "description": "Tarifa AT/EP del código CNAE",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TarifaAtEpDetail" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PaidRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/obligaciones/{code_type}/{code}": {
      "get": {
        "operationId": "getObligaciones",
        "summary": "Obligaciones fiscales de un código",
        "description": "Devuelve las obligaciones fiscales (modelos AEAT) aplicables a un código IAE o CNAE. Disponible en el plan Free (requiere una clave de API, cualquier plan sirve). Si el código no tiene modelos aplicables, el endpoint responde 200 con la lista `obligaciones` vacía y `total: 0` (no devuelve 404).",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "$ref": "#/components/parameters/CodeTypePath" },
          { "$ref": "#/components/parameters/GenericCodePath" }
        ],
        "responses": {
          "200": {
            "description": "Obligaciones fiscales aplicables",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ObligacionesResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/bulk": {
      "post": {
        "operationId": "bulkLookup",
        "summary": "Lookup masivo de códigos",
        "description": "Consulta hasta 50 códigos IAE o CNAE en una sola petición. Requiere plan Profesional o superior. Devuelve los resultados en el mismo orden que se solicitan, con `found: false` para los no encontrados.",
        "tags": ["Códigos"],
        "security": [{ "ApiKeyAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BulkRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultados del lookup masivo",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BulkResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PaidRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/calendario/{code_type}/{code}": {
      "get": {
        "operationId": "getCalendario",
        "summary": "Calendario fiscal (iCalendar)",
        "description": "Devuelve un archivo iCalendar (RFC 5545, `text/calendar`) con los plazos AEAT aplicables al código, para suscripción en cualquier app de calendario. Endpoint público (sin autenticación). El código admite sufijo `.ics`. Cacheado 1 hora.",
        "tags": ["Fiscal"],
        "security": [],
        "parameters": [
          { "$ref": "#/components/parameters/CodeTypePath" },
          { "$ref": "#/components/parameters/CalendarioCodePath" }
        ],
        "responses": {
          "200": {
            "description": "Archivo iCalendar con los plazos fiscales",
            "headers": {
              "Cache-Control": {
                "description": "Política de caché.",
                "schema": { "type": "string", "example": "public, max-age=3600, s-maxage=3600" }
              },
              "Content-Disposition": {
                "description": "Nombre de archivo sugerido.",
                "schema": { "type": "string", "example": "inline; filename=\"calendario-iae-5013.ics\"" }
              }
            },
            "content": {
              "text/calendar": {
                "schema": { "type": "string", "format": "binary" },
                "example": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Conversor IAE CNAE//Calendario Fiscal//ES\r\nEND:VCALENDAR\r\n"
              }
            }
          },
          "400": {
            "description": "Tipo de código no válido (debe ser `iae` o `cnae`).",
            "content": {
              "text/plain": {
                "schema": { "type": "string", "example": "Invalid code_type" }
              }
            }
          },
          "404": {
            "description": "No hay modelos/plazos aplicables al código.",
            "content": {
              "text/plain": {
                "schema": { "type": "string", "example": "No applicable modelos found" }
              }
            }
          },
          "500": {
            "description": "Error al obtener los plazos.",
            "content": {
              "text/plain": {
                "schema": { "type": "string", "example": "Error fetching plazos" }
              }
            }
          }
        }
      }
    },
    "/signup": {
      "post": {
        "operationId": "createApiKey",
        "summary": "Crear una API key gratuita",
        "description": "Crea una API key del plan Free (250 peticiones/día, sin tarjeta) y la envía por email. Endpoint público con límite de 5 peticiones/hora por IP. La clave completa solo se devuelve una vez en la respuesta.",
        "tags": ["Cuenta"],
        "security": [],
        "parameters": [
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Atribución de canal de alta (p. ej. `docs`, `homepage_cta`). Máx. 64 caracteres.",
            "schema": { "type": "string", "maxLength": 64, "example": "docs" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SignupRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "API key creada y enviada por email",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SignupResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequestPublic" },
          "409": {
            "description": "Este email ya tiene una API key activa.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublicError" }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimitedPublic" },
          "500": { "$ref": "#/components/responses/ServerErrorPublic" }
        }
      }
    },
    "/subscribe": {
      "post": {
        "operationId": "createSubscription",
        "summary": "Suscribirse a un plan de pago",
        "description": "Crea una sesión de Stripe Checkout para un plan de pago (Profesional o Empresa) y devuelve la URL de pago. Endpoint público con límite por IP.",
        "tags": ["Cuenta"],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SubscribeRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sesión de Checkout creada",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SubscribeResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequestPublic" },
          "429": { "$ref": "#/components/responses/RateLimitedPublic" },
          "500": { "$ref": "#/components/responses/ServerErrorPublic" }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Consultar uso de la API key",
        "description": "Devuelve el uso actual de la clave (consumo del día, límite, plan, endpoints más usados). No consume cuota diaria. Requiere la cabecera `X-API-Key`. Tiene límite por IP para evitar enumeración de claves.",
        "tags": ["Cuenta"],
        "security": [{ "ApiKeyAuth": [] }],
        "responses": {
          "200": {
            "description": "Estado de uso de la clave",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/UsageResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedPublic" },
          "403": {
            "description": "API key desactivada.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublicError" }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimitedPublic" },
          "500": { "$ref": "#/components/responses/ServerErrorPublic" }
        }
      }
    },
    "/keys": {
      "post": {
        "operationId": "rotateOrRevokeApiKey",
        "summary": "Rotar o revocar tu propia API key",
        "description": "Permite al titular de una clave rotarla (genera una clave nueva; la anterior deja de funcionar de inmediato) o revocarla (desactivación inmediata) sin pasar por soporte. Autenticación por posesión: se valida la cabecera `X-API-Key` con la clave ACTUAL — no se admite ninguna otra credencial ni sesión. Disponible para cualquier plan. La clave nueva devuelta en `rotate` solo se muestra una vez en la respuesta: no se reenvía por email ni se registra en logs. Límite de 30 peticiones/hora por IP.",
        "tags": ["Cuenta"],
        "security": [{ "ApiKeyAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/KeysRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acción aplicada correctamente",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/KeysResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequestPublic" },
          "401": { "$ref": "#/components/responses/UnauthorizedPublic" },
          "429": { "$ref": "#/components/responses/RateLimitedPublic" },
          "500": { "$ref": "#/components/responses/ServerErrorPublic" }
        }
      }
    },
    "/kyb/enrich": {
      "post": {
        "operationId": "kybEnrich",
        "summary": "Enriquecer actividad económica para KYB bancario",
        "description": "Vista KYB (Know Your Business) para entidades financieras. Devuelve la clasificación IAE/CNAE del cliente, perfil fiscal completo (sector de riesgo, regímenes IVA/IRPF, retenciones, casillas Modelo 036) y obligaciones fiscales aplicables. Requiere plan Profesional o superior. Envía `code` (+ `code_type` opcional) O `activity_text` (2–200 caracteres), nunca ambos a la vez.",
        "tags": ["B2B"],
        "security": [{ "ApiKeyAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ActivityLookupRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Perfil fiscal completo para KYB",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/KybEnrichResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PaidRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/onboarding/activity": {
      "post": {
        "operationId": "onboardingActivity",
        "summary": "Configurar actividad para onboarding SaaS / facturación",
        "description": "Vista de onboarding para SaaS de facturación y ERPs. Dado un código IAE/CNAE o texto de actividad libre, devuelve toda la configuración fiscal necesaria para pre-rellenar un alta de autónomo o empresa: epígrafe IAE, casillas Modelo 036, tipo y régimen de IVA, retención IRPF (tipo general y tipo inicio de actividad), régimen recomendado y próximos plazos fiscales. Requiere plan Profesional o superior. Envía `code` (+ `code_type` opcional) O `activity_text` (2–200 caracteres), nunca ambos a la vez.",
        "tags": ["B2B"],
        "security": [{ "ApiKeyAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ActivityLookupRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Configuración fiscal para onboarding",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OnboardingActivityResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/PaidRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/subvenciones": {
      "get": {
        "operationId": "listSubvenciones",
        "summary": "Subvenciones BDNS abiertas por código CNAE",
        "description": "Devuelve convocatorias de subvenciones (BDNS) aplicables a un código CNAE 2025. El parámetro `cnae` es obligatorio. Con clave gratuita solo se honra `cnae`: la respuesta se limita a 5 resultados con campos de descubrimiento (`codigo_bdns`, `titulo`, `organo`, `nivel1`, `cnae_seccion`, `url_oficial`, `importe`, `estado`). Con plan Profesional o superior se honran además `region`, `estado`, `solo_actividad`, `importe_min`, `limit` (máx. 100) y `offset`, y la respuesta incluye todos los campos (`finalidad`, `tipos_beneficiarios`, `regiones`, `instrumentos`, `sectores`, `fecha_apertura`, `fecha_recepcion`, `url_bases`, `abierto`). `importe` es la dotación total de la convocatoria (no por beneficiario). El campo `fecha_cierre` nunca se expone — el dato de BDNS es poco fiable como plazo y no debe usarse para tomar decisiones de fecha límite. Nota sobre filtros y paginación: `region`, `estado` e `importe_min` se aplican sobre la ventana de filas ya obtenida (hasta `offset + limit`, con techo de 200), no sobre el conjunto completo de convocatorias del CNAE — una respuesta filtrada puede devolver menos filas que `limit`, y paginar más allá de esa ventana no garantiza cobertura exhaustiva.",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "cnae",
            "in": "query",
            "required": true,
            "description": "Código CNAE 2025 con o sin puntos (p. ej. `47.11` o `4711`).",
            "schema": { "type": "string", "example": "4711" }
          },
          {
            "name": "region",
            "in": "query",
            "required": false,
            "description": "Filtra por Comunidad Autónoma (nombre normalizado, p. ej. `Madrid`). Solo plan Profesional o superior.",
            "schema": { "type": "string", "example": "Madrid" }
          },
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "description": "Filtra por estado (`abierto`, `cerrado`, u otro valor libre comparado contra el campo `estado`). Solo plan Profesional o superior. Nota: este endpoint solo expone convocatorias ABIERTAS, por lo que filtrar por un estado cerrado (p. ej. `cerrado`) siempre devuelve un resultado vacío.",
            "schema": { "type": "string", "example": "abierto" }
          },
          {
            "name": "solo_actividad",
            "in": "query",
            "required": false,
            "description": "Si es `true`, limita a convocatorias cuyos beneficiarios encajan con autónomos/pymes (señal `tipos_beneficiarios`, no `es_para_empresas`). Solo plan Profesional o superior.",
            "schema": { "type": "boolean", "default": false }
          },
          {
            "name": "importe_min",
            "in": "query",
            "required": false,
            "description": "Importe (dotación) mínimo de la convocatoria. Solo plan Profesional o superior.",
            "schema": { "type": "number", "example": 100000 }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Número máximo de resultados (1–100, por defecto 50). Ignorado en plan Free (fijo en 5). Solo plan Profesional o superior.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Desplazamiento de paginación. Se aplica sobre una ventana máxima de 200 filas — offsets mayores devuelven una página vacía. Solo plan Profesional o superior.",
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Subvenciones BDNS aplicables al código CNAE",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SubvencionesResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/legislacion": {
      "get": {
        "operationId": "searchLegislacion",
        "summary": "Búsqueda de texto legislativo BOE consolidado",
        "description": "Búsqueda de texto completo (FTS en español) sobre el corpus de artículos vigentes del BOE consolidado que alimenta las páginas públicas `/legislacion`. El parámetro `q` es obligatorio (consulta en lenguaje natural, p. ej. `alta autónomo actividad económica`). Con clave gratuita solo se honra `q`: la respuesta se limita a los 3 resultados más relevantes con campos de descubrimiento (`boe_id`, `articulo_norm`, `bloque_titulo`, `snippet` — el `snippet` es un fragmento HTML con los términos coincidentes resaltados en `<b>`); no se expone `rank` (puntuación interna de relevancia) ni se admite filtro por norma o paginación. Con plan Profesional o superior se honran además `boe_id` (limita la búsqueda a una norma concreta), `limit` (máx. 100, por defecto 50) y `offset` (paginación real a nivel de base de datos, no una ventana recortada), y cada resultado incluye `rank`. `_meta` siempre incluye `plan` y `remaining`; en plan Free añade `note` explicando el techo de 3 resultados; en Profesional+ añade `offset` y, si se aplicó, `boe_id`.",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Consulta en lenguaje natural (español). Si falta o está vacía, responde 400.",
            "schema": { "type": "string", "example": "alta autónomo actividad económica" }
          },
          {
            "name": "boe_id",
            "in": "query",
            "required": false,
            "description": "Limita la búsqueda a una norma concreta (p. ej. `BOE-A-2006-20764`). Solo plan Profesional o superior — ignorado en plan Free.",
            "schema": { "type": "string", "example": "BOE-A-2006-20764" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Número máximo de resultados (1–100, por defecto 50). Ignorado en plan Free (fijo en 3). Solo plan Profesional o superior.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Desplazamiento de paginación, aplicado directamente en base de datos (sin techo artificial de ventana). Solo plan Profesional o superior — ignorado en plan Free.",
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultados de la búsqueda de texto legislativo",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "q": { "type": "string" },
                    "resultados": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "boe_id": { "type": "string", "example": "BOE-A-2006-20764" },
                          "articulo_norm": { "type": "string", "example": "31" },
                          "bloque_titulo": { "type": "string" },
                          "snippet": { "type": "string", "description": "Fragmento HTML con los términos coincidentes envueltos en <b>." },
                          "rank": { "type": "number", "description": "Puntuación interna de relevancia (ts_rank). Solo plan Profesional o superior — ausente en la respuesta Free." }
                        }
                      }
                    },
                    "total": { "type": "integer" },
                    "_meta": {
                      "type": "object",
                      "properties": {
                        "plan": { "type": "string" },
                        "count": { "type": "integer" },
                        "remaining": { "type": "integer" },
                        "note": { "type": "string", "description": "Solo en plan Free — explica el techo de 3 resultados." },
                        "offset": { "type": "integer", "description": "Solo Profesional+ — el offset realmente aplicado." },
                        "boe_id": { "type": "string", "description": "Solo Profesional+ — el filtro por norma aplicado, si lo hubo." },
                        "upgrade_hint": { "type": "string" }
                      }
                    }
                  }
                },
                "example": {
                  "q": "alta autónomo actividad económica",
                  "resultados": [
                    {
                      "boe_id": "BOE-A-2006-20764",
                      "articulo_norm": "31",
                      "bloque_titulo": "Artículo 31. Método de estimación objetiva.",
                      "snippet": "…el <b>alta</b> en el <b>autónomo</b> régimen de estimación objetiva…"
                    }
                  ],
                  "total": 1,
                  "_meta": { "plan": "free", "count": 1, "remaining": 249, "note": "Vista limitada a 3 resultados…" }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/legislacion/{boe_id}": {
      "get": {
        "operationId": "getLegislacionArticulo",
        "summary": "Artículo consolidado del BOE (y grafo de hechos en Profesional+)",
        "description": "Devuelve el texto vigente de un artículo del BOE consolidado (`boe_id` en la ruta, p. ej. `BOE-A-2006-20764`; `articulo` como query, p. ej. `31`). `articulo` es obligatorio salvo que se use `full=true` con plan Profesional o superior (para volcar la norma completa sin fijar un artículo base) — si falta sin esa combinación, responde 400. Si el artículo no existe vigente para esa norma, responde 200 con `articulo_texto: null` y `_meta.nota` (nunca 404). Con clave gratuita solo se devuelve `articulo_texto` (el mismo texto público que renderizan las páginas `/legislacion`); los flags de pago `facts`, `full` y `historial` se ignoran silenciosamente y `_meta.upgrade_hint` indica que requieren plan Profesional o superior. Con plan Profesional o superior: `facts=true` añade el grafo de hechos (Fact↔Law) del artículo, `historial=true` añade el historial de versiones, y `full=true` añade `norma_bloques` con todos los artículos vigentes de la norma (si se combina con la ausencia de `articulo`, la respuesta es solo `{ boe_id, norma_bloques, _meta }`, sin `articulo_texto`).",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "boe_id",
            "in": "path",
            "required": true,
            "description": "Identificador BOE de la norma (p. ej. `BOE-A-2006-20764`).",
            "schema": { "type": "string", "example": "BOE-A-2006-20764" }
          },
          {
            "name": "articulo",
            "in": "query",
            "required": false,
            "description": "Número de artículo (p. ej. `31`). Obligatorio salvo que se use `full=true` con plan Profesional o superior; si falta sin esa combinación, responde 400.",
            "schema": { "type": "string", "example": "31" }
          },
          {
            "name": "facts",
            "in": "query",
            "required": false,
            "description": "Si es `true`, añade el grafo de hechos (Fact↔Law) del artículo. Solo plan Profesional o superior — ignorado en plan Free.",
            "schema": { "type": "boolean", "default": false }
          },
          {
            "name": "full",
            "in": "query",
            "required": false,
            "description": "Si es `true`, añade (o, sin `articulo`, devuelve en solitario) `norma_bloques` con todos los artículos vigentes de la norma. Solo plan Profesional o superior — ignorado en plan Free.",
            "schema": { "type": "boolean", "default": false }
          },
          {
            "name": "historial",
            "in": "query",
            "required": false,
            "description": "Si es `true`, añade el historial de versiones del artículo. Solo plan Profesional o superior — ignorado en plan Free.",
            "schema": { "type": "boolean", "default": false }
          }
        ],
        "responses": {
          "200": {
            "description": "Artículo consolidado (y, en Profesional+, grafo de hechos / norma completa / historial cuando se solicitan)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "boe_id": { "type": "string" },
                    "articulo_texto": {
                      "nullable": true,
                      "type": "object",
                      "description": "Texto vigente del artículo, o null si no se encontró vigente para esta norma (ver _meta.nota). Ausente en el modo bulk (full=true sin articulo).",
                      "properties": {
                        "bloque_id": { "type": "string" },
                        "bloque_titulo": { "type": "string" },
                        "articulo_norm": { "type": "string", "nullable": true },
                        "version_idx": { "type": "integer" },
                        "fecha_vigencia": { "type": "string", "nullable": true, "format": "date" },
                        "valid_to": { "type": "string", "nullable": true, "format": "date" },
                        "es_vigente": { "type": "boolean" },
                        "texto_html": { "type": "string" },
                        "id_norma_modificadora": { "type": "string", "nullable": true }
                      }
                    },
                    "facts": {
                      "type": "array",
                      "description": "Solo Profesional+ y facts=true — grafo de hechos (Fact↔Law) del artículo.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "fact_domain": { "type": "string" },
                          "fact_key": { "type": "string" },
                          "valor": {},
                          "unidad": { "type": "string", "nullable": true },
                          "articulo": { "type": "string", "nullable": true },
                          "source_anchor": { "type": "string", "nullable": true },
                          "vigente": { "type": "boolean" },
                          "valid_from": { "type": "string", "nullable": true, "format": "date" },
                          "valid_to": { "type": "string", "nullable": true, "format": "date" }
                        }
                      }
                    },
                    "historial": {
                      "type": "array",
                      "description": "Solo Profesional+ y historial=true — historial de versiones del artículo.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "version_idx": { "type": "integer" },
                          "bloque_titulo": { "type": "string" },
                          "fecha_vigencia": { "type": "string", "nullable": true, "format": "date" },
                          "valid_to": { "type": "string", "nullable": true, "format": "date" },
                          "es_vigente": { "type": "boolean" },
                          "id_norma_modificadora": { "type": "string", "nullable": true }
                        }
                      }
                    },
                    "norma_bloques": {
                      "type": "array",
                      "description": "Solo Profesional+ y full=true — todos los artículos vigentes de la norma.",
                      "items": { "type": "object" }
                    },
                    "_meta": {
                      "type": "object",
                      "properties": {
                        "plan": { "type": "string" },
                        "count": { "type": "integer" },
                        "remaining": { "type": "integer" },
                        "nota": { "type": "string", "description": "Presente cuando el artículo solicitado no se encontró vigente para esta norma." },
                        "upgrade_hint": { "type": "string" }
                      }
                    }
                  }
                },
                "example": {
                  "boe_id": "BOE-A-2006-20764",
                  "articulo_texto": {
                    "bloque_id": "a31",
                    "bloque_titulo": "Artículo 31. Método de estimación objetiva.",
                    "articulo_norm": "31",
                    "version_idx": 3,
                    "fecha_vigencia": "2023-01-01",
                    "valid_to": null,
                    "es_vigente": true,
                    "texto_html": "<p>…</p>",
                    "id_norma_modificadora": null
                  },
                  "_meta": { "plan": "free", "count": 1, "remaining": 249, "upgrade_hint": "El grafo de hechos (facts), el histórico de versiones (historial) y la norma completa (full) requieren un plan Profesional o superior." }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/empresas/{cnae}": {
      "get": {
        "operationId": "getEmpresasPorActividad",
        "summary": "Firmografía de empresas constituidas por código CNAE (BORME)",
        "description": "Devuelve estadísticas de constitución de empresas (BORME) para un código CNAE 2025: recuento total, capital medio/mediano, tendencia mensual y provincias — datos agregados a nivel de empresa (Ley 37/2007), sin datos personales. El parámetro de ruta `cnae` es obligatorio. Con clave gratuita los agregados se devuelven íntegros, pero la lista de constituciones recientes se limita a 3 con campos básicos (`denominacion`, `provincia`). Con plan Profesional o superior se honran `limit` (máx. 100) y `offset`, y cada empresa incluye `capital_eur`, `published_at` y `cnae_primary`. Nota de paginación: `offset` se aplica sobre una ventana máxima de 200 filas — offsets mayores devuelven una página vacía.",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          { "name": "cnae", "in": "path", "required": true, "description": "Código CNAE 2025 con o sin puntos (p. ej. `68.20` o `6820`).", "schema": { "type": "string", "pattern": "^[\\d.]+$", "example": "6820" } },
          { "name": "limit", "in": "query", "required": false, "description": "Número máximo de constituciones recientes (1–100, por defecto 50). Ignorado en plan Free (fijo en 3). Solo plan Profesional o superior.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } },
          { "name": "offset", "in": "query", "required": false, "description": "Desplazamiento de paginación sobre `recent`. Se aplica sobre una ventana máxima de 200 filas — offsets mayores devuelven una página vacía. Solo plan Profesional o superior.", "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": {
          "200": { "description": "Firmografía de empresas para el código CNAE", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmpresasResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/dgt": {
      "get": {
        "operationId": "searchDgtConsultas",
        "summary": "Búsqueda de consultas vinculantes de la DGT",
        "description": "Búsqueda de texto completo sobre consultas vinculantes y generales de la Dirección General de Tributos (`dgt_consultas`). El parámetro `q` es obligatorio. Con clave gratuita solo se honra `q`: la respuesta se limita a 3 resultados con campos de descubrimiento (`titulo_seo`, `resumen`), sin filtro por código ni paginación. Con plan Profesional o superior se honran además `code` (filtra por código IAE asociado), `limit` (máx. 100, por defecto 20) y `offset`, y la respuesta incluye todos los campos (`numero`, `slug`, `titulo_seo`, `resumen`, `fecha`, `organo`, `rank`).",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Texto de búsqueda libre sobre las consultas de la DGT.",
            "schema": { "type": "string", "example": "arrendamiento de inmuebles" }
          },
          {
            "name": "code",
            "in": "query",
            "required": false,
            "description": "Filtra por código IAE asociado. Debe ir con puntos (p. ej. `501.3`) — el filtro compara contra los códigos IAE almacenados en formato con puntos; un código sin puntos no devuelve resultados. Solo plan Profesional o superior.",
            "schema": { "type": "string", "example": "501.3" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Número máximo de resultados (1–100, por defecto 20). Ignorado en plan Free (fijo en 3). Solo plan Profesional o superior.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Desplazamiento de paginación (paginación nativa de la RPC subyacente). Solo plan Profesional o superior.",
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Consultas de la DGT que coinciden con la búsqueda",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DgtSearchResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/dgt/{slug}": {
      "get": {
        "operationId": "getDgtConsultaBySlug",
        "summary": "Detalle de una consulta vinculante de la DGT",
        "description": "Devuelve el detalle de una consulta de la DGT identificada por su `slug` (no el `numero`, p. ej. `V1234-23`, que no es válido en una URL). Con clave gratuita se devuelve identidad y resumen (`titulo_seo`, `resumen`, `normativa`); el texto íntegro (`hechos`, `cuestion`, `contestacion`) se omite por completo. Con plan Profesional o superior se devuelve además el texto íntegro de la consulta, `normas_citadas` y `codes_iae`.",
        "tags": ["Fiscal"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Slug de la consulta DGT (identificador URL-safe, distinto del número de consulta).",
            "schema": { "type": "string", "example": "v1234-23-arrendamiento-inmuebles" }
          }
        ],
        "responses": {
          "200": {
            "description": "Detalle de la consulta DGT",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DgtDetailResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Clave de API con prefijo `cvr_`. Obtén una gratis en POST /signup."
      }
    },
    "parameters": {
      "IaeCodePath": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "Código IAE con o sin puntos (p. ej. `501.3` o `5013`).",
        "schema": { "type": "string", "pattern": "^[\\d.]+$", "example": "5013" }
      },
      "CnaeCodePath": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "Código CNAE 2025 con o sin puntos (p. ej. `68.20` o `6820`).",
        "schema": { "type": "string", "pattern": "^[\\d.]+$", "example": "6820" }
      },
      "Cnae2009CodePath": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "Código CNAE 2009 con o sin puntos (máx. 20 caracteres).",
        "schema": { "type": "string", "pattern": "^[\\d.]+$", "maxLength": 20, "example": "4711" }
      },
      "GenericCodePath": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "Código IAE o CNAE con o sin puntos (solo dígitos y puntos, máx. 20).",
        "schema": { "type": "string", "pattern": "^[\\d.]+$", "maxLength": 20, "example": "5013" }
      },
      "CalendarioCodePath": {
        "name": "code",
        "in": "path",
        "required": true,
        "description": "Código IAE o CNAE con o sin puntos (solo dígitos y puntos, máx. 20). Admite sufijo `.ics` (p. ej. `5013.ics`).",
        "schema": { "type": "string", "pattern": "^[\\d.]+$", "maxLength": 20, "example": "5013" }
      },
      "CodeTypePath": {
        "name": "code_type",
        "in": "path",
        "required": true,
        "description": "Tipo de catálogo del código.",
        "schema": { "type": "string", "enum": ["iae", "cnae"], "example": "iae" }
      },
      "SlugQuery": {
        "name": "slug",
        "in": "query",
        "required": false,
        "description": "Slug para desambiguar cuando un código numérico mapea a varias filas. 1–120 caracteres, `^[a-z0-9-]+$`.",
        "schema": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[a-z0-9-]+$", "example": "albanileria" }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Petición inválida (parámetro o cuerpo mal formado).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "Invalid slug format", "_meta": { "plan": "free", "remaining": 248 } }
          }
        }
      },
      "BadRequestPublic": {
        "description": "Petición inválida (datos o cuerpo mal formado).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/PublicError" }
          }
        }
      },
      "Unauthorized": {
        "description": "Falta la cabecera `X-API-Key` o la clave es inválida/desactivada.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "Missing X-API-Key header" }
          }
        }
      },
      "UnauthorizedPublic": {
        "description": "Clave ausente, con formato inválido, o no encontrada.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/PublicError" }
          }
        }
      },
      "PaidRequired": {
        "description": "El endpoint requiere un plan de pago (Profesional o superior).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/PaidRequiredError" },
            "example": {
              "error": "Este endpoint requiere un plan de pago (Profesional o superior).",
              "current_plan": "free",
              "upgrade_url": "https://www.conversoriaecnae.es/precios"
            }
          }
        }
      },
      "NotFound": {
        "description": "Código no encontrado.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "IAE code not found", "_meta": { "plan": "free", "remaining": 247 } }
          }
        }
      },
      "AmbiguousCode": {
        "description": "El código mapea a varias filas. Usa `?slug=` (de `candidates[].url`) para desambiguar.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/AmbiguousCodeError" }
          }
        }
      },
      "RateLimited": {
        "description": "Límite de cuota diaria o de ráfaga superado. La cabecera `Retry-After` indica los segundos hasta poder reintentar.",
        "headers": {
          "Retry-After": {
            "description": "Segundos hasta el siguiente reintento permitido (ráfaga: 1–12 s hasta el siguiente token, según plan; cuota diaria: hasta medianoche Europe/Madrid).",
            "schema": { "type": "integer", "example": 3600 }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/RateLimitError" }
          }
        }
      },
      "RateLimitedPublic": {
        "description": "Demasiadas peticiones desde tu IP. Reintenta más tarde.",
        "headers": {
          "Retry-After": {
            "description": "Segundos sugeridos hasta el siguiente reintento (cuando aplica).",
            "schema": { "type": "integer", "example": 3600 }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/PublicError" }
          }
        }
      },
      "ServerError": {
        "description": "Error interno del servidor.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "Internal error" }
          }
        }
      },
      "ServerErrorPublic": {
        "description": "Error interno del servidor.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/PublicError" }
          }
        }
      }
    },
    "schemas": {
      "Meta": {
        "type": "object",
        "description": "Metadatos del envelope para endpoints con clave: plan actual, cuota restante y nudge de upgrade (solo plan Free al ≥80% de cuota).",
        "required": ["plan", "remaining"],
        "properties": {
          "plan": { "type": "string", "example": "free", "description": "Plan de la clave (free, profesional, empresa, enterprise)." },
          "remaining": { "type": "integer", "example": 248, "description": "Peticiones restantes de la cuota diaria." },
          "upgrade_hint": {
            "type": "string",
            "description": "Mensaje legible de upgrade. Presente solo en plan Free al consumir ≥80% de la cuota.",
            "example": "Has usado más del 80% de tu cuota diaria. Mejora a Profesional por 29€/mes para 2000 consultas/día: https://www.conversoriaecnae.es/precios"
          },
          "upgrade": { "$ref": "#/components/schemas/UpgradeMeta" }
        }
      },
      "UpgradeMeta": {
        "type": "object",
        "description": "Forma estructurada del nudge de upgrade para renderizar UI nativa. Presente solo en plan Free al ≥80% de cuota.",
        "properties": {
          "hint": { "type": "string" },
          "url": { "type": "string", "format": "uri", "example": "https://www.conversoriaecnae.es/precios" },
          "current_plan": { "type": "string", "example": "free" },
          "recommended_plan": {
            "type": "object",
            "properties": {
              "id": { "type": "string", "example": "profesional" },
              "label": { "type": "string", "example": "Profesional" },
              "daily_limit": { "type": "integer", "example": 2000 },
              "monthly_price_eur": { "type": "integer", "example": 29 }
            }
          },
          "daily_limit_after": { "type": "integer", "example": 2000 },
          "price_eur_monthly": { "type": "integer", "example": 29 }
        }
      },
      "Error": {
        "type": "object",
        "description": "Cuerpo de error estándar de endpoints con clave.",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "example": "IAE code not found" },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "PublicError": {
        "type": "object",
        "description": "Cuerpo de error de endpoints públicos / de cuenta.",
        "required": ["success", "error"],
        "properties": {
          "success": { "type": "boolean", "example": false },
          "error": { "type": "string", "example": "Datos inválidos." },
          "fields": {
            "type": "object",
            "additionalProperties": { "type": "array", "items": { "type": "string" } },
            "description": "Errores de validación por campo (cuando aplica)."
          }
        }
      },
      "PaidRequiredError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "example": "Este endpoint requiere un plan de pago (Profesional o superior)." },
          "current_plan": { "type": "string", "example": "free" },
          "upgrade_url": { "type": "string", "format": "uri", "example": "https://www.conversoriaecnae.es/precios" }
        }
      },
      "RateLimitError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "example": "Rate limit exceeded" },
          "limit": { "type": "integer", "example": 250 },
          "plan": { "type": "string", "example": "free" },
          "retry_after_seconds": { "type": "integer", "example": 3600 },
          "current_plan_label": { "type": "string", "example": "Free (250 peticiones/día)" },
          "upgrade_url": { "type": "string", "format": "uri", "example": "https://www.conversoriaecnae.es/precios" },
          "recommended_plan": {
            "type": "object",
            "properties": {
              "id": { "type": "string", "example": "profesional" },
              "label": { "type": "string", "example": "Profesional" },
              "daily_limit": { "type": "integer", "example": 2000 },
              "monthly_price_eur": { "type": "integer", "example": 29 }
            }
          }
        }
      },
      "AmbiguousCodeError": {
        "type": "object",
        "required": ["error", "code", "candidates"],
        "properties": {
          "error": { "type": "string", "example": "ambiguous_code" },
          "code": { "type": "string", "example": "834" },
          "message": { "type": "string", "example": "Multiple IAE rows match this code. Use ?slug= to disambiguate." },
          "candidates": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AmbiguousCandidate" }
          },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "AmbiguousCandidate": {
        "type": "object",
        "description": "Una de las filas que coinciden con el código ambiguo. Sigue `url` para la consulta desambiguada.",
        "properties": {
          "code": { "type": "string", "example": "834", "description": "Código con puntos." },
          "cod_integro": { "type": "string", "nullable": true, "example": "834" },
          "slug": { "type": "string", "example": "servicios-relativos-a-la-propiedad-inmobiliaria" },
          "title": { "type": "string", "nullable": true, "example": "Servicios relativos a la propiedad inmobiliaria" },
          "url": { "type": "string", "example": "/api/v1/iae/834?slug=servicios-relativos-a-la-propiedad-inmobiliaria" }
        }
      },
      "IaeDetail": {
        "type": "object",
        "description": "Detalle de un código IAE.",
        "properties": {
          "code": { "type": "string", "example": "501.3", "description": "Código con puntos." },
          "cod_integro": { "type": "string", "nullable": true, "example": "501.3" },
          "slug": { "type": "string", "example": "albanileria-y-pequenos-trabajos-de-construccion" },
          "seccion_label": { "type": "string", "nullable": true, "example": "Sección Primera: Actividades Empresariales" },
          "title": { "type": "string", "nullable": true, "example": "Albañilería y pequeños trabajos de construcción en general" },
          "description": { "type": "string", "nullable": true },
          "category": { "type": "string", "nullable": true, "example": "Construcción" },
          "level": { "type": "integer", "nullable": true, "example": 3 },
          "modulos_eligible": { "type": "boolean", "nullable": true, "example": true },
          "associated_cnae": { "type": "array", "nullable": true, "items": {}, "description": "Códigos CNAE 2025 asociados (solo plan Profesional o superior)." },
          "related_iae": { "type": "array", "nullable": true, "items": {}, "description": "Códigos IAE relacionados (solo plan Profesional o superior)." },
          "keywords": { "type": "array", "nullable": true, "items": {}, "description": "Palabras clave del epígrafe." },
          "last_updated": { "type": "string", "nullable": true, "example": "2026-01-15" },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "CnaeDetail": {
        "type": "object",
        "description": "Detalle de un código CNAE 2025.",
        "properties": {
          "code": { "type": "string", "example": "68.20", "description": "Código con puntos." },
          "title": { "type": "string", "nullable": true, "example": "Alquiler de bienes inmobiliarios por cuenta propia" },
          "description": { "type": "string", "nullable": true },
          "category": { "type": "string", "nullable": true },
          "level": { "type": "integer", "nullable": true, "example": 4 },
          "associated_iae": { "type": "array", "nullable": true, "items": {}, "description": "Códigos IAE asociados (solo plan Profesional o superior)." },
          "related_cnae": { "type": "array", "nullable": true, "items": {}, "description": "Códigos CNAE relacionados (solo plan Profesional o superior)." },
          "cnae_2009": { "type": "array", "nullable": true, "items": {}, "description": "Equivalencias CNAE 2009 (solo plan Profesional o superior)." },
          "keywords": { "type": "array", "nullable": true, "items": {}, "description": "Palabras clave." },
          "last_updated": { "type": "string", "nullable": true, "example": "2026-01-15" },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "Cnae2009Correspondence": {
        "type": "object",
        "description": "Correspondencias CNAE 2009 → CNAE 2025.",
        "properties": {
          "cnae_2009": { "type": "string", "example": "4711", "description": "Código CNAE 2009 normalizado (sin puntos)." },
          "correspondences": {
            "type": "array",
            "description": "Filas de correspondencia hacia CNAE 2025.",
            "items": { "type": "object", "additionalProperties": true }
          },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "query": { "type": "string", "example": "albañilería" },
          "type": { "type": "string", "enum": ["iae", "cnae", "both"], "example": "both" },
          "count": { "type": "integer", "example": 12 },
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/SearchResult" }
          },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "SearchResult": {
        "type": "object",
        "description": "Resultado de búsqueda. Incluye las columnas del RPC `search_*_codes` más un campo `type`.",
        "additionalProperties": true,
        "properties": {
          "type": { "type": "string", "enum": ["iae", "cnae"], "example": "iae" }
        }
      },
      "ModulosDetail": {
        "type": "object",
        "description": "Régimen de módulos (estimación objetiva) de un código IAE.",
        "properties": {
          "code": { "type": "string", "example": "5013", "description": "Código normalizado, sin puntos (este endpoint devuelve la forma normalizada, a diferencia de /iae/{code} que devuelve la forma con puntos)." },
          "slug": { "type": "string", "example": "albanileria-y-pequenos-trabajos-de-construccion" },
          "titulo": { "type": "string", "nullable": true, "example": "Albañilería y pequeños trabajos de construcción" },
          "modulos_eligible": { "type": "boolean", "nullable": true, "example": true },
          "regimen": { "$ref": "#/components/schemas/Regimen" },
          "citation": {
            "type": "object",
            "nullable": true,
            "description": "Cita de fuente y frescura del régimen de módulos, leída del sidecar `modulos_snapshot` (Orden HAC/1425/2025). `null` cuando no hay snapshot sincronizado para este código — no es un error.",
            "properties": {
              "orden": { "type": "string", "nullable": true, "example": "Orden HAC/1425/2025" },
              "source_anchor": { "type": "string", "nullable": true, "example": "Orden HAC/1425/2025, Anexo II" },
              "ejercicio": { "type": "integer", "nullable": true, "example": 2026 },
              "verified_at": { "type": "string", "format": "date-time", "nullable": true, "example": "2026-06-01T00:00:00.000Z" }
            }
          },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "Regimen": {
        "type": "object",
        "properties": {
          "actividad": { "type": "object", "nullable": true, "additionalProperties": true },
          "modulos_irpf": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "modulos_iva": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "umbrales_actividad": { "type": "object", "nullable": true, "additionalProperties": true },
          "umbrales_generales": { "type": "object", "nullable": true, "additionalProperties": true },
          "intelligence": {
            "type": "object",
            "nullable": true,
            "properties": {
              "modulos_summary": { "type": "string", "nullable": true },
              "calculo_hints": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "primary_modulos": { "type": "array", "items": {} },
                  "common_pitfalls": { "type": "array", "items": {} }
                }
              },
              "citations": { "type": "array", "items": {} }
            }
          }
        }
      },
      "TarifaAtEpDetail": {
        "type": "object",
        "description": "Tarifa AT/EP (cotización por cuenta ajena) de un código CNAE 2025.",
        "properties": {
          "code": { "type": "string", "example": "6820", "description": "Código CNAE consultado, normalizado sin puntos." },
          "cnae_code": { "type": "string", "example": "6820", "description": "Código CNAE efectivamente resuelto (puede ser un prefijo del código consultado, ver `resolved_via`)." },
          "ejercicio": { "type": "integer", "example": 2026 },
          "tipo_it": { "type": "number", "nullable": true, "example": 1.5, "description": "Tipo de Incapacidad Temporal, en %." },
          "tipo_ims": { "type": "number", "nullable": true, "example": 1.1, "description": "Tipo de Incapacidad Muerte y Supervivencia, en %." },
          "tipo_total": { "type": "number", "nullable": true, "example": 2.6, "description": "Suma tipo_it + tipo_ims, en %." },
          "descripcion": { "type": "string", "nullable": true, "example": "Alquiler de bienes inmobiliarios por cuenta propia" },
          "source_anchor": { "type": "string", "nullable": true, "example": "D.A. 61ª TRLGSS, Cuadro I" },
          "resolved_via": { "type": "string", "enum": ["exact", "prefix3", "prefix2"], "example": "exact", "description": "Nivel de resolución usado para encontrar la tarifa: coincidencia exacta, o fallback a prefijo de 3 o 2 dígitos." },
          "aplicacion": { "type": "string", "enum": ["cuenta_ajena"], "example": "cuenta_ajena", "description": "Esta tarifa aplica a trabajadores POR CUENTA AJENA (empleados). El autónomo cotiza AT/EP al tipo fijo indicado en `nota_reta`, no a este tipo." },
          "nota_reta": { "type": "string", "example": "1.30", "description": "Tipo fijo (%) que aplica el autónomo (RETA) por AT/EP, independiente de la actividad — no confundir con `tipo_total`." },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "SubvencionesResponse": {
        "type": "object",
        "description": "Convocatorias de subvenciones BDNS aplicables a un código CNAE. La forma de cada elemento de `subvenciones` depende del plan — ver SubvencionDiscoveryItem (Free) y SubvencionFullItem (Profesional+).",
        "properties": {
          "cnae": { "type": "string", "example": "4711", "description": "Código CNAE consultado, tal y como se envió." },
          "subvenciones": {
            "type": "array",
            "items": {
              "oneOf": [
                { "$ref": "#/components/schemas/SubvencionDiscoveryItem" },
                { "$ref": "#/components/schemas/SubvencionFullItem" }
              ]
            }
          },
          "total": { "type": "integer", "example": 3, "description": "Número de filas devueltas en esta página." },
          "_meta": { "$ref": "#/components/schemas/SubvencionesMeta" }
        }
      },
      "SubvencionesMeta": {
        "allOf": [
          { "$ref": "#/components/schemas/Meta" },
          {
            "type": "object",
            "properties": {
              "count": { "type": "integer", "example": 3 },
              "note": { "type": "string", "nullable": true, "description": "Presente solo en plan Free — explica el tope de 5 filas y que los filtros requieren plan Profesional o superior." },
              "offset": { "type": "integer", "nullable": true, "description": "Presente solo en plan Profesional o superior — el offset efectivamente aplicado." }
            }
          }
        ]
      },
      "SubvencionDiscoveryItem": {
        "type": "object",
        "description": "Proyección de campos de descubrimiento (plan Free) — máx. 5 filas por respuesta.",
        "properties": {
          "codigo_bdns": { "type": "string", "example": "800123" },
          "titulo": { "type": "string", "example": "Ayudas a la digitalización de pymes" },
          "organo": { "type": "string", "nullable": true, "example": "Ministerio de Industria" },
          "nivel1": { "type": "string", "nullable": true, "example": "Estado" },
          "cnae_seccion": { "type": "string", "nullable": true, "example": "G", "description": "Primera sección CNAE asociada a la convocatoria." },
          "url_oficial": { "type": "string", "nullable": true, "format": "uri" },
          "importe": { "type": "number", "nullable": true, "example": 5000000, "description": "Dotación total de la convocatoria (no por beneficiario)." },
          "estado": { "type": "string", "nullable": true, "example": "abierto" }
        }
      },
      "SubvencionFullItem": {
        "type": "object",
        "description": "Proyección completa (plan Profesional o superior). NUNCA incluye `fecha_cierre` — el dato de BDNS es poco fiable como plazo.",
        "properties": {
          "codigo_bdns": { "type": "string", "example": "800123" },
          "titulo": { "type": "string", "example": "Ayudas a la digitalización de pymes" },
          "finalidad": { "type": "string", "nullable": true },
          "organo": { "type": "string", "nullable": true, "example": "Ministerio de Industria" },
          "nivel1": { "type": "string", "nullable": true, "example": "Estado" },
          "importe": { "type": "number", "nullable": true, "example": 5000000, "description": "Dotación total de la convocatoria (no por beneficiario)." },
          "fecha_apertura": { "type": "string", "nullable": true, "example": "2026-01-15" },
          "fecha_recepcion": { "type": "string", "nullable": true, "example": "2026-01-10" },
          "abierto": { "type": "boolean", "example": true },
          "estado": { "type": "string", "nullable": true, "example": "abierto" },
          "tipo_convocatoria": { "type": "string", "nullable": true, "example": "concurrencia competitiva" },
          "sectores": { "description": "Array jsonb de {codigo, descripcion} (estructura variable)." },
          "cnae_secciones": { "type": "array", "items": { "type": "string" }, "example": ["G"] },
          "tipos_beneficiarios": { "type": "array", "items": { "type": "string" }, "example": ["Autónomos", "Pymes"], "description": "Señal de autónomo/pyme — úsala en vez de es_para_empresas." },
          "instrumentos": { "type": "array", "items": { "type": "string" }, "example": ["Subvención"] },
          "regiones": { "type": "array", "items": { "type": "string" }, "example": ["ES30 - MADRID"], "description": "Valores libres de BDNS; normalízalos con normalizeRegionToCCAA si necesitas el nombre canónico de la CCAA." },
          "url_bases": { "type": "string", "nullable": true, "format": "uri" },
          "url_oficial": { "type": "string", "nullable": true, "format": "uri" }
        }
      },
      "EmpresasResponse": {
        "type": "object",
        "description": "Firmografía BORME por CNAE. Los agregados (recuento, capital, tendencia, provincias) son libres; la forma de cada elemento de `firmographics.recent` depende del plan — ver BormeRecentTeaser (Free) y BormeRecentFull (Profesional+).",
        "properties": {
          "cnae": { "type": "string", "example": "6820", "description": "Código CNAE consultado, tal y como se envió." },
          "firmographics": { "$ref": "#/components/schemas/EmpresasFirmographics" },
          "_meta": { "$ref": "#/components/schemas/EmpresasMeta" }
        }
      },
      "EmpresasFirmographics": {
        "type": "object",
        "properties": {
          "code": { "type": "string", "example": "6820" },
          "total_count": { "type": "integer", "example": 1234 },
          "capital_avg": { "type": "number", "nullable": true, "example": 15000 },
          "capital_median": { "type": "number", "nullable": true, "example": 3000 },
          "first_published": { "type": "string", "nullable": true, "example": "2024-01-05" },
          "last_published": { "type": "string", "nullable": true, "example": "2026-06-30" },
          "monthly": { "type": "array", "items": { "type": "object", "properties": { "month": { "type": "string", "example": "2026-06" }, "count": { "type": "integer", "example": 42 } } } },
          "top_provincias": { "type": "array", "items": { "type": "object", "properties": { "provincia": { "type": "string", "example": "Madrid" }, "count": { "type": "integer", "example": 300 } } } },
          "recent": { "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/BormeRecentTeaser" }, { "$ref": "#/components/schemas/BormeRecentFull" } ] } }
        }
      },
      "EmpresasMeta": {
        "allOf": [
          { "$ref": "#/components/schemas/Meta" },
          { "type": "object", "properties": {
            "count": { "type": "integer", "example": 3, "description": "Número de constituciones recientes devueltas en esta página." },
            "note": { "type": "string", "nullable": true, "description": "Presente solo en plan Free — explica el tope de 3 filas y los campos básicos." },
            "offset": { "type": "integer", "nullable": true, "description": "Presente solo en plan Profesional o superior — el offset efectivamente aplicado." }
          } }
        ]
      },
      "BormeRecentTeaser": {
        "type": "object",
        "description": "Proyección básica de una constitución reciente (plan Free) — máx. 3 filas.",
        "properties": {
          "denominacion": { "type": "string", "example": "EJEMPLO SL" },
          "provincia": { "type": "string", "nullable": true, "example": "Madrid" }
        }
      },
      "BormeRecentFull": {
        "type": "object",
        "description": "Proyección completa de una constitución reciente (plan Profesional o superior).",
        "properties": {
          "denominacion": { "type": "string", "example": "EJEMPLO SL" },
          "provincia": { "type": "string", "nullable": true, "example": "Madrid" },
          "capital_eur": { "type": "number", "nullable": true, "example": 3000 },
          "published_at": { "type": "string", "nullable": true, "example": "2026-06-15" },
          "cnae_primary": { "type": "string", "nullable": true, "example": "6820" }
        }
      },
      "ActivitySubvencionesBlock": {
        "type": "object",
        "nullable": true,
        "description": "Subvenciones BDNS/SNPSAP abiertas para la sección CNAE de la actividad resuelta. `null` cuando la actividad no tiene correspondencia CNAE 2025 (no se pudo derivar sección) o si el dato no está disponible. NUNCA incluye `fecha_cierre` — el dato de BDNS es poco fiable como plazo y no debe usarse para tomar decisiones de fecha límite.",
        "properties": {
          "seccion": { "type": "string", "nullable": true, "example": "G", "description": "Sección CNAE (A–U) derivada del código CNAE 2025 resuelto." },
          "abiertas_count": { "type": "integer", "example": 42, "description": "Convocatorias abiertas en esta sección CNAE." },
          "para_actividad_economica_count": { "type": "integer", "example": 17, "description": "Subconjunto de abiertas_count dirigido a autónomos/pyme/empresa (tipos_beneficiarios)." },
          "destacadas": {
            "type": "array",
            "maxItems": 3,
            "description": "Hasta 3 convocatorias abiertas más relevantes para la actividad.",
            "items": {
              "type": "object",
              "properties": {
                "codigo_bdns": { "type": "string", "example": "800123" },
                "titulo": { "type": "string", "example": "Ayudas a la digitalización de pymes" },
                "organo": { "type": "string", "nullable": true, "example": "Red.es" },
                "importe": { "type": "number", "nullable": true, "example": 250000, "description": "Dotación de la convocatoria (no por beneficiario)." },
                "url_oficial": { "type": "string", "nullable": true, "format": "uri" }
              }
            }
          },
          "fuente": { "type": "string", "example": "BDNS / SNPSAP (IGAE)" }
        }
      },
      "DgtSearchResponse": {
        "type": "object",
        "description": "Resultado de la búsqueda de consultas vinculantes de la DGT. La forma de cada elemento de `consultas` depende del plan — ver DgtDiscoveryItem (Free) y DgtSearchFullItem (Profesional+).",
        "properties": {
          "query": { "type": "string", "example": "arrendamiento de inmuebles", "description": "Texto de búsqueda enviado." },
          "consultas": {
            "type": "array",
            "items": {
              "oneOf": [
                { "$ref": "#/components/schemas/DgtDiscoveryItem" },
                { "$ref": "#/components/schemas/DgtSearchFullItem" }
              ]
            }
          },
          "total": { "type": "integer", "example": 3, "description": "Número de filas devueltas en esta página." },
          "_meta": { "$ref": "#/components/schemas/DgtSearchMeta" }
        }
      },
      "DgtSearchMeta": {
        "allOf": [
          { "$ref": "#/components/schemas/Meta" },
          {
            "type": "object",
            "properties": {
              "count": { "type": "integer", "example": 3 },
              "note": { "type": "string", "nullable": true, "description": "Presente solo en plan Free — explica el tope de 3 filas y que el filtro por código y la paginación requieren plan Profesional o superior." },
              "offset": { "type": "integer", "nullable": true, "description": "Presente solo en plan Profesional o superior — el offset efectivamente aplicado." }
            }
          }
        ]
      },
      "DgtDiscoveryItem": {
        "type": "object",
        "description": "Proyección de campos de descubrimiento (plan Free) — máx. 3 filas por respuesta.",
        "properties": {
          "titulo_seo": { "type": "string", "nullable": true, "example": "Tributación del arrendamiento de inmuebles por autónomos" },
          "resumen": { "type": "string", "nullable": true, "example": "La DGT concluye que el arrendamiento de inmuebles tributa por el epígrafe 861 del IAE cuando..." }
        }
      },
      "DgtSearchFullItem": {
        "type": "object",
        "description": "Proyección completa (plan Profesional o superior).",
        "properties": {
          "numero": { "type": "string", "example": "V1234-23" },
          "slug": { "type": "string", "example": "v1234-23-arrendamiento-inmuebles" },
          "titulo_seo": { "type": "string", "nullable": true, "example": "Tributación del arrendamiento de inmuebles por autónomos" },
          "resumen": { "type": "string", "nullable": true, "example": "La DGT concluye que el arrendamiento de inmuebles tributa por el epígrafe 861 del IAE cuando..." },
          "fecha": { "type": "string", "nullable": true, "example": "2023-05-12" },
          "organo": { "type": "string", "nullable": true, "example": "SG de Impuestos sobre la Renta de las Personas Físicas" },
          "rank": { "type": "number", "example": 0.87, "description": "Relevancia de la búsqueda de texto completo (mayor es más relevante)." }
        }
      },
      "DgtDetailResponse": {
        "type": "object",
        "description": "Detalle de una consulta DGT. Con plan Free solo se devuelven los campos de DgtDetailFreeItem (identidad + resumen); con plan Profesional o superior se añade el texto íntegro (hechos, cuestión y contestación vinculante).",
        "properties": {
          "numero": { "type": "string", "example": "V1234-23" },
          "slug": { "type": "string", "example": "v1234-23-arrendamiento-inmuebles" },
          "tipo": { "type": "string", "enum": ["vinculante", "general"], "example": "vinculante" },
          "fecha": { "type": "string", "nullable": true, "example": "2023-05-12" },
          "organo": { "type": "string", "nullable": true, "example": "SG de Impuestos sobre la Renta de las Personas Físicas" },
          "titulo_seo": { "type": "string", "nullable": true, "example": "Tributación del arrendamiento de inmuebles por autónomos" },
          "resumen": { "type": "string", "nullable": true, "example": "La DGT concluye que el arrendamiento de inmuebles tributa por el epígrafe 861 del IAE cuando..." },
          "normativa": { "type": "string", "nullable": true, "example": "Art. 78 y 79 RDLeg 1175/1990" },
          "hechos": { "type": "string", "nullable": true, "description": "Solo plan Profesional o superior. Antecedentes de hecho planteados en la consulta." },
          "cuestion": { "type": "string", "nullable": true, "description": "Solo plan Profesional o superior. Cuestión planteada a la DGT." },
          "contestacion": { "type": "string", "description": "Solo plan Profesional o superior. Contestación íntegra de la DGT — la conclusión vinculante se sitúa al FINAL del texto, no al principio." },
          "normas_citadas": { "type": "array", "items": { "type": "string" }, "description": "Solo plan Profesional o superior. Normas citadas en la contestación." },
          "codes_iae": { "type": "array", "items": { "type": "string" }, "description": "Solo plan Profesional o superior. Epígrafes IAE relacionados con la consulta." },
          "_meta": { "$ref": "#/components/schemas/DgtDetailMeta" }
        }
      },
      "DgtDetailMeta": {
        "allOf": [
          { "$ref": "#/components/schemas/Meta" },
          {
            "type": "object",
            "properties": {
              "note": { "type": "string", "nullable": true, "description": "Presente solo en plan Free — explica que el texto íntegro requiere plan Profesional o superior." }
            }
          }
        ]
      },
      "ObligacionesResponse": {
        "type": "object",
        "properties": {
          "code": { "type": "string", "example": "5013" },
          "type": { "type": "string", "enum": ["iae", "cnae"], "example": "iae" },
          "obligaciones": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Obligacion" }
          },
          "total": { "type": "integer", "example": 4 },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "Obligacion": {
        "type": "object",
        "description": "Una obligación fiscal (modelo AEAT) aplicable al código.",
        "properties": {
          "modelo_id": { "type": "string", "example": "303" },
          "name": { "type": "string", "nullable": true, "example": "IVA — Autoliquidación trimestral" },
          "periodicidad": { "type": "string", "nullable": true, "example": "trimestral" },
          "confidence": { "type": "number", "nullable": true, "example": 0.95 },
          "reason": { "type": "string", "nullable": true, "example": "Actividad sujeta a IVA" },
          "fecha_proximo_plazo": { "type": "string", "nullable": true, "example": "2026-07-20" }
        }
      },
      "BulkRequest": {
        "type": "object",
        "required": ["codes", "type"],
        "properties": {
          "codes": {
            "type": "array",
            "description": "Lista de códigos (1–50). Cada uno solo dígitos y puntos, máx. 20 caracteres.",
            "minItems": 1,
            "maxItems": 50,
            "items": { "type": "string", "pattern": "^[\\d.]+$", "maxLength": 20 },
            "example": ["501.3", "504.1", "505.4"]
          },
          "type": { "type": "string", "enum": ["iae", "cnae"], "example": "iae" }
        }
      },
      "BulkResponse": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "enum": ["iae", "cnae"], "example": "iae" },
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/BulkResult" }
          },
          "total": { "type": "integer", "example": 3 },
          "found": { "type": "integer", "example": 2 },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "BulkResult": {
        "type": "object",
        "required": ["code", "found"],
        "properties": {
          "code": { "type": "string", "example": "501.3" },
          "found": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Fila completa del catálogo cuando `found` es true; ausente cuando es false. Para `type=cnae`, el campo `iae_correspondiente` contiene códigos IAE cualificados por sección (`cod_integro_iae`, p. ej. `\"A011\"`), no códigos numéricos simples — esto desambigua los epígrafes que comparten número entre secciones A/B/C. Para reutilizar uno en `GET /iae/{code}`, quita la letra de sección inicial (`\"A011\"` → `\"011\"`). Para `type=iae`, el campo `relaciones_iae` contiene códigos IAE relacionados cualificados por sección (`cod_integro_iae`, p. ej. `\"A011\"`), no códigos numéricos simples — el mismo criterio de desambiguación. Para reutilizar uno en `GET /iae/{code}`, quita igualmente la letra de sección inicial (`\"A011\"` → `\"011\"`)."
          }
        }
      },
      "SignupRequest": {
        "type": "object",
        "required": ["email", "name", "terms_accepted"],
        "properties": {
          "email": { "type": "string", "format": "email", "maxLength": 254, "example": "dev@ejemplo.es" },
          "name": { "type": "string", "minLength": 1, "maxLength": 100, "example": "Ana García" },
          "terms_accepted": { "type": "boolean", "enum": [true], "example": true, "description": "Debe ser `true`; aceptación de los términos de uso." }
        }
      },
      "SignupResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "message": { "type": "string", "example": "Tu API key ha sido enviada a tu email." },
          "api_key": { "type": "string", "example": "cvr_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "description": "Clave completa. Solo se devuelve una vez." },
          "masked_key": { "type": "string", "example": "cvr_a1b2c3d4****************************************************************" }
        }
      },
      "SubscribeRequest": {
        "type": "object",
        "required": ["email", "name", "plan"],
        "properties": {
          "email": { "type": "string", "format": "email", "maxLength": 254, "example": "dev@ejemplo.es" },
          "name": { "type": "string", "minLength": 1, "maxLength": 100, "example": "Ana García" },
          "plan": { "type": "string", "enum": ["profesional", "empresa"], "example": "profesional" },
          "billing_cycle": { "type": "string", "enum": ["monthly", "annual"], "default": "monthly", "example": "monthly" },
          "utm_source": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[a-z0-9_-]+$", "example": "landing_api" },
          "email_lead_id": { "type": "string", "format": "uuid" }
        }
      },
      "SubscribeResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "url": { "type": "string", "format": "uri", "description": "URL de Stripe Checkout.", "example": "https://checkout.stripe.com/c/pay/cs_test_..." }
        }
      },
      "UsageResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "data": {
            "type": "object",
            "properties": {
              "plan": { "type": "string", "example": "free" },
              "requests_used_today": { "type": "integer", "example": 12 },
              "rate_limit_per_day": { "type": "integer", "example": 250 },
              "remaining_today": { "type": "integer", "example": 238 },
              "is_active": { "type": "boolean", "example": true },
              "created_at": { "type": "string", "format": "date-time", "example": "2026-05-01T10:00:00Z" },
              "last_used_at": { "type": "string", "format": "date-time", "nullable": true, "example": "2026-06-02T09:15:00Z" },
              "top_endpoints": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "endpoint": { "type": "string", "example": "/api/v1/search" },
                    "count": { "type": "integer", "example": 8 }
                  }
                }
              }
            }
          }
        }
      },
      "KeysRequest": {
        "type": "object",
        "required": ["action"],
        "properties": {
          "action": { "type": "string", "enum": ["rotate", "revoke"], "example": "rotate", "description": "Acción a realizar: rotate (genera una clave nueva) o revoke (desactiva la clave actual)." }
        }
      },
      "KeysResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "example": true },
          "api_key": { "type": "string", "example": "cvr_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4", "description": "Presente solo cuando action=rotate. Clave nueva completa, se devuelve una única vez." },
          "message": { "type": "string", "example": "Clave rotada. Guárdala: no se vuelve a mostrar." }
        }
      },
      "ActivityLookupRequest": {
        "type": "object",
        "description": "Cuerpo de petición compartido por `/kyb/enrich` y `/onboarding/activity`. Envía un epígrafe IAE via `code` con `code_type:\"iae\"` (o sin `code_type`, ya que `\"iae\"` es el valor por defecto) O describe la actividad con `activity_text` (2–200 caracteres). Nunca envíes `code` y `activity_text` a la vez (devuelve 400). La búsqueda directa por código CNAE (`code_type:\"cnae\"`) no está soportada en estos endpoints — devuelve 400; usa el epígrafe IAE correspondiente o `activity_text`.",
        "properties": {
          "code": {
            "type": "string",
            "pattern": "^[\\dA-Za-z.]+$",
            "maxLength": 20,
            "example": "5013",
            "description": "Epígrafe IAE con o sin puntos (solo alfanuméricos y puntos, máx. 20 caracteres). Mutuamente excluyente con `activity_text`. Úsalo siempre junto a `code_type:\"iae\"` (o sin `code_type`); `code_type:\"cnae\"` devuelve 400 en estos endpoints."
          },
          "code_type": {
            "type": "string",
            "enum": ["iae", "cnae"],
            "example": "iae",
            "description": "Tipo de catálogo del código. Opcional; por defecto `\"iae\"`. Solo se admite `\"iae\"` en estos endpoints — enviar `\"cnae\"` devuelve 400. Para buscar por actividad sin conocer el epígrafe IAE, usa `activity_text` en su lugar."
          },
          "activity_text": {
            "type": "string",
            "minLength": 2,
            "maxLength": 200,
            "example": "reformas de albañilería",
            "description": "Descripción libre de la actividad económica (2–200 caracteres). Mutuamente excluyente con `code`."
          }
        }
      },
      "ActivityMatch": {
        "type": "object",
        "description": "Resultado de la resolución del código o actividad.",
        "properties": {
          "confidence": { "type": "number", "nullable": true, "example": 0.97, "description": "Confianza del match (0.0–1.0). null para lexical/no-match." },
          "method": { "type": "string", "enum": ["exact_code", "semantic", "lexical"], "example": "semantic", "description": "Método de resolución: `exact_code` (código exacto), `semantic` (búsqueda semántica) o `lexical` (búsqueda léxica sin embedding)." },
          "alternatives": {
            "type": "array",
            "description": "Alternativas de menor confianza cuando el match semántico es ambiguo.",
            "items": {
              "type": "object",
              "properties": {
                "code": { "type": "string", "example": "504.1" },
                "titulo": { "type": "string", "example": "Instalaciones eléctricas en general" },
                "type": { "type": "string", "enum": ["iae", "cnae"], "example": "iae" },
                "score": { "type": "number", "nullable": true, "example": 0.71 }
              }
            }
          }
        }
      },
      "ActivityClassification": {
        "type": "object",
        "description": "Clasificación IAE y CNAE 2025 del match.",
        "properties": {
          "iae": {
            "type": "object",
            "nullable": true,
            "properties": {
              "code": { "type": "string", "example": "501.3", "description": "Código con puntos." },
              "codeNormalized": { "type": "string", "example": "5013", "description": "Código sin puntos (normalizado)." },
              "titulo": { "type": "string", "example": "Albañilería y pequeños trabajos de construcción" }
            }
          },
          "cnae2025": {
            "type": "object",
            "nullable": true,
            "properties": {
              "code": { "type": "string", "example": "43.99", "description": "Código con puntos." },
              "codeNormalized": { "type": "string", "example": "4399", "description": "Código sin puntos (normalizado)." },
              "titulo": { "type": "string", "example": "Otras actividades de construcción especializada" }
            }
          }
        }
      },
      "KybEnrichResponse": {
        "type": "object",
        "description": "Respuesta del endpoint `/kyb/enrich`.",
        "properties": {
          "match": { "$ref": "#/components/schemas/ActivityMatch" },
          "classification": { "$ref": "#/components/schemas/ActivityClassification" },
          "fiscal_profile": {
            "type": "object",
            "description": "Perfil fiscal de la actividad para evaluación de riesgo KYB.",
            "properties": {
              "sector_riesgo": { "type": "string", "example": "medio", "description": "Nivel de riesgo fiscal del sector (bajo, medio, alto)." },
              "iva_regimen": { "type": "string", "example": "general", "description": "Régimen de IVA aplicable." },
              "irpf_regimen": { "type": "string", "example": "estimacion_directa", "description": "Régimen de IRPF aplicable." },
              "retencion_irpf_pct": { "type": "number", "nullable": true, "example": 15, "description": "Porcentaje de retención IRPF aplicable." },
              "modelo_036_casillas_relevantes": {
                "type": "array",
                "items": { "type": "string" },
                "example": ["601", "602"],
                "description": "Casillas del Modelo 036 relevantes para esta actividad."
              }
            }
          },
          "obligaciones": {
            "type": "array",
            "description": "Obligaciones fiscales (modelos AEAT) aplicables.",
            "items": { "$ref": "#/components/schemas/Obligacion" }
          },
          "subvenciones": { "$ref": "#/components/schemas/ActivitySubvencionesBlock" },
          "audit": {
            "type": "object",
            "description": "Metadatos de auditoría y trazabilidad de la respuesta.",
            "properties": {
              "source": { "type": "string", "example": "conversoriaecnae.es", "description": "Identificador de la fuente de los datos." },
              "sources_oficiales": {
                "type": "array",
                "items": { "type": "string" },
                "example": ["AEAT", "BOE", "INE"],
                "description": "Fuentes oficiales que respaldan la clasificación."
              },
              "data_version": { "type": "string", "nullable": true, "example": "2026-06-01T10:00:00Z", "description": "Versión del catálogo usado (ISO timestamp)." },
              "generated_at": { "type": "string", "format": "date-time", "example": "2026-06-12T10:00:00Z" },
              "data_residency": { "type": "string", "example": "EU (eu-central-2)", "description": "Región de residencia de los datos (RGPD)." }
            }
          },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      },
      "OnboardingActivityResponse": {
        "type": "object",
        "description": "Respuesta del endpoint `/onboarding/activity`.",
        "properties": {
          "match": { "$ref": "#/components/schemas/ActivityMatch" },
          "classification": { "$ref": "#/components/schemas/ActivityClassification" },
          "alta_036": {
            "type": "object",
            "description": "Datos para pre-rellenar el Modelo 036 de alta.",
            "properties": {
              "epigrafe_iae": { "type": "string", "nullable": true, "example": "501.3", "description": "Epígrafe IAE a consignar en el Modelo 036." },
              "casillas_relevantes": {
                "type": "array",
                "items": { "type": "string" },
                "example": ["601", "602"],
                "description": "Casillas del Modelo 036 relevantes para esta alta."
              }
            }
          },
          "facturacion": {
            "type": "object",
            "description": "Configuración de facturación para pre-rellenar el SaaS.",
            "properties": {
              "iva_tipo": { "oneOf": [{ "type": "number" }, { "type": "string", "enum": ["exento"] }], "nullable": true, "example": 21, "description": "Tipo de IVA aplicable (número, p. ej. 21, o 'exento')." },
              "iva_regimen": { "type": "string", "nullable": true, "example": "general", "description": "Régimen de IVA aplicable." },
              "retencion_irpf": {
                "type": "object",
                "properties": {
                  "tipo": { "type": "number", "nullable": true, "example": 15, "description": "Tipo de retención IRPF general (número, p. ej. 15)." },
                  "tipo_inicio_actividad": { "type": "number", "nullable": true, "example": 7, "description": "Tipo reducido para los 3 primeros años de actividad (p. ej. 7). Null cuando no aplica." }
                }
              },
              "verified": { "type": "boolean", "example": true, "description": "Si la configuración está verificada contra fuentes oficiales." }
            }
          },
          "regimen_recomendado": {
            "type": "object",
            "nullable": true,
            "description": "Régimen fiscal recomendado para esta actividad. Null si no se pudo determinar.",
            "properties": {
              "modulos_elegible": { "type": "boolean", "example": false },
              "modulos_summary": { "type": "string", "nullable": true, "example": "No apto para módulos" },
              "calculo_hints": { "nullable": true, "description": "Hints de cálculo (estructura variable)." },
              "verified": { "type": "boolean", "example": true }
            }
          },
          "calendario": {
            "type": "array",
            "description": "Próximos plazos fiscales aplicables (misma estructura que Obligacion).",
            "items": { "$ref": "#/components/schemas/Obligacion" }
          },
          "subvenciones": { "$ref": "#/components/schemas/ActivitySubvencionesBlock" },
          "reta": {
            "type": "object",
            "description": "Información orientativa sobre la cuota RETA.",
            "properties": {
              "tarifa_plana_eur_mes": { "type": "number", "nullable": true, "example": 80, "description": "Cuota de tarifa plana (€/mes). Null si no aplica." },
              "base_minima_aplica": { "type": "boolean", "example": true, "description": "Si se puede optar por la base mínima RETA." },
              "nota": { "type": "string", "example": "Cuota de partida (tarifa plana): punto de partida del alta. La cuota definitiva depende del rendimiento neto anual.", "description": "Nota informativa sobre la cuota RETA." },
              "mei_eur_mes": { "type": "number", "nullable": true, "example": null, "description": "Cuota MEI mensual (€) cuando aplica." },
              "total_primer_periodo_eur_mes": { "type": "number", "nullable": true, "example": null, "description": "Total primer periodo RETA (€/mes) cuando aplica." },
              "source_anchor": { "type": "string", "nullable": true, "example": null, "description": "Referencia normativa de la tarifa plana." }
            }
          },
          "_meta": { "$ref": "#/components/schemas/Meta" }
        }
      }
    }
  }
}
